/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Nodes, dom } from '@knno/dom';
import { AdaptivePopup, isMobile } from '../adaptivePopup/adaptivePopup';
import time from '@knno/time';
import { calendar } from '../calendar/calendar';
import { datePickerStyle } from './datePicker.css';
import { PickGroup, PickOptions, PickUpdateOption, PickValue, picker } from '../picker/picker';
import { Toast } from '../toast/toast';
import '../../dict';
import { DATA } from '../symbol';
import { DICT } from '../../dict';

export type DateMode = 'dy' | 'dm' | 'dd' | 'th' | 'tm' | 'ts' | 'dd+th' | 'dd+tm' | 'dd+ts';

const FORMAT = {
	dy: 'yyyy',
	dm: 'yyyy MM',
	dd: 'yyyy MM dd',
	th: 'HH',
	tm: 'HH mm',
	ts: 'HH mm ss',
	'dd+th': 'yyyy MM dd HH',
	'dd+tm': 'yyyy MM dd HH mm',
	'dd+ts': 'yyyy MM dd HH mm ss',
};

function getYears(): string[] {
	return Array(200)
		.fill(0)
		.map((_, i) => (1970 + i + '').padStart(4, '0'));
}

function getMonths(): string[] {
	return Array(12)
		.fill(0)
		.map((_, i) => (i + 1 + '').padStart(2, '0'));
}

function getHours(): string[] {
	return Array(24)
		.fill(0)
		.map((_, i) => (i + '').padStart(2, '0'));
}

function getMinOrSec(): string[] {
	return Array(60)
		.fill(0)
		.map((_, i) => (i + '').padStart(2, '0'));
}

function getDays(selection: PickValue[][], option: PickUpdateOption): string[] {
	const selGroup = selection[option.gid];
	let days: number;
	if (!selGroup) {
		days = 31;
	} else {
		const m = parseInt(selGroup[1] + '');
		if ([1, 3, 5, 7, 8, 10, 12].includes(m)) {
			days = 31;
		} else if ([4, 6, 9, 11].includes(m)) {
			days = 30;
		} else {
			days = 29;
		}
	}
	option.keepPosition = true;
	return Array(days)
		.fill(0)
		.map((_, i) => (i + 1 + '').padStart(2, '0'));
}

type PickHandler = (value: Date | Date[]) => void;

interface PickObject {
	container: Nodes;
	title: string;
	type: 'calendar' | 'picker';
	select: () => void;
	focus: () => void;
	onSelect: (handler: PickHandler) => void;
}

interface DatePickHandler {
	onSelect: (callback: PickHandler) => this;
	onClose: (callback: () => void) => this;
	close: () => void;
}

type OpenSelectOptions = {
	/**
	 * Which node should the float window will align to it, if not provide refer than forcely use action sheet as the operation UI instead of float window.
	 */
	refer?: Nodes;
	/**
	 * default value
	 */
	date?: Date[];
	/**
	 * Indicate whether it's a choosen of the time range, default is false
	 */
	range?: boolean;
	/**
	 * default is dd
	 */
	mode?: DateMode;
	focus?: boolean;
};

export class DatePicker {
	public static parseDate(date: string | null, mode: DateMode): Date | null {
		if (!date) {
			return null;
		}
		const fmt = FORMAT[mode];
		let t: Date | null;
		t = time.parse(date, fmt);
		if (!t) {
			t = time.parse(date) ?? null;
		}
		return t;
	}

	public static trimDateString(date: string | null, mode: DateMode): string | null {
		const fmt = FORMAT[mode];
		const t = DatePicker.parseDate(date, mode);
		return t ? time.format(t, fmt) : null;
	}

	public static formatDate<T extends Date | null, R extends T extends Date ? string : null>(
		date: T,
		mode: DateMode
	): R {
		if (date instanceof Date) {
			const fmt = FORMAT[mode];
			return time.format(date, fmt) as R;
		} else {
			return null as R;
		}
	}

	private static makePicker(title: string, date: Date[], isRange: boolean, mode: DateMode): PickObject {
		// use picker
		const dateOptions: PickOptions[] = [];
		const fmt = FORMAT[mode];
		if (/^d/.test(mode)) {
			dateOptions.push(getYears());
		}
		if (/d[dm]/.test(mode)) {
			dateOptions.push(getMonths());
		}
		if (/dd/.test(mode)) {
			dateOptions.push(getDays); // dynamically get days
		}
		if (/t/.test(mode)) {
			dateOptions.push(getHours());
		}
		if (/t[ms]/.test(mode)) {
			dateOptions.push(getMinOrSec());
		}
		if (/s/.test(mode)) {
			dateOptions.push(getMinOrSec());
		}
		const groups: PickGroup[] = [
			{
				name: DICT.from,
				parts: dateOptions,
			},
		];
		if (isRange) {
			groups.push({
				name: DICT.to,
				parts: dateOptions,
			});
		}
		let pickHandler: PickHandler;
		const pk = picker('popup').optionGroups(groups);
		for (let i = 2 - date.length; i > 0; i--) {
			date.push(new Date());
		}
		pk.value(
			date.map((d) => {
				return time.format(d, fmt)!.split(' ');
			})
		);
		pk.on('change', () => {
			if (isRange) {
				const gid = pk.activeGroup();
				const values = getDateValue();
				if (gid === 0) {
					if (values[1] < values[0]) {
						values[1] = values[0];
						pk[DATA].selection = values.map((d) => {
							return time.format(d, fmt)!.split(' ');
						});
					}
				} else if (gid === 1) {
					if (values[0] > values[1]) {
						values[0] = values[1];
						pk[DATA].selection = values.map((d) => {
							return time.format(d, fmt)!.split(' ');
						});
					}
				}
			}
		});
		function getDateValue() {
			const values = pk.value();
			const value = [time.parse(values[0].join(' '), fmt)!];
			if (isRange) {
				value.push(time.parse(values[1].join(' '), fmt)!);
			}
			return value;
		}
		function doPick() {
			const values = getDateValue();
			if (isRange) {
				pickHandler(values);
			} else {
				pickHandler(values[0]);
			}
		}
		return {
			type: 'picker',
			title,
			container: dom.div(datePickerStyle).append(pk),
			select() {
				doPick();
			},
			focus() {
				pk.focus();
			},
			onSelect(handler) {
				pickHandler = handler;
			},
		};
	}

	private static makeCalendar(title: string, date: Date[], isRange: boolean, mode: DateMode): PickObject {
		const cal = calendar().range(isRange).value(date);
		if (mode === 'dy') {
			cal.mode('year');
		} else if (mode === 'dm') {
			cal.mode('month');
		} else {
			cal.mode('date');
		}
		if (!isRange) {
			cal.on('pick', () => {
				doPick();
			});
		}
		let pickHandler: PickHandler;
		function doPick() {
			const val = cal.value();
			if (val === null) {
				Toast.show(DICT.notChoosen);
				return;
			}
			pickHandler(val);
		}
		return {
			type: 'calendar',
			title,
			container: dom.div(datePickerStyle).append(cal),
			select() {
				// do pick;
				doPick();
			},
			focus() {
				cal.focus();
			},
			onSelect(handler) {
				pickHandler = handler;
			},
		};
	}

	private static makeContainer(date: Date[], isRange: boolean, mode: DateMode): PickObject {
		const isTime = /t/.test(mode);
		const useActionSheet = isMobile();
		let title: string;
		if (isTime) {
			if (isRange) {
				title = DICT.chooseTimeRange;
			} else {
				title = DICT.chooseTime;
			}
		} else {
			if (isRange) {
				title = DICT.chooseDateRange;
			} else {
				title = DICT.chooseDate;
			}
		}
		if (useActionSheet || isTime) {
			return this.makePicker(title, date, isRange, mode);
		} else {
			return this.makeCalendar(title, date, isRange, mode);
		}
	}

	static show({
		refer,
		date = [],
		range = false,
		mode = 'dd',
		focus = false,
	}: OpenSelectOptions = {}): DatePickHandler {
		let onSelect: PickHandler | undefined;
		let onClose: (() => void) | undefined;
		const pickHandler = this.makeContainer(date, range, mode);
		pickHandler.onSelect((value) => {
			if (value instanceof Array) {
				value.sort((a, b) => a.getTime() - b.getTime());
			}
			onSelect?.(value);
			pop.close();
			refer?.focus();
		});
		const pop = AdaptivePopup.show(pickHandler.container, {
			refer,
			focus: false,
			extraClass: 'no-h-padding no-t-padding',
			useAction: true,
			width: 'auto',
			noActionBar: pickHandler.type === 'calendar' && !range ? true : false,
			title: pickHandler.title,
		})
			.onOpen(() => {
				if (focus) pickHandler.focus();
			})
			.onClose(() => {
				onClose?.();
			})
			.onOK(() => {
				pickHandler.select();
			});
		return {
			close() {
				pop.close();
			},
			onClose(callback) {
				onClose = callback;
				return this;
			},
			onSelect(callback) {
				onSelect = callback;
				return this;
			},
		};
	}
}
